﻿using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
using System.Xml;
using System.Net;

namespace GateKeeper
{
    public static class Utils
    {
        //public static XmlDocument XmlApplicationLog;
        public static XmlDocument xmlWhiteList;
        public static XmlDocument xmlBlackList;
        public static XmlDocument xmlHoneyPotList;
        public static XmlDocument xmlHttpBLList;
        public static XmlDocument xmlProxyBLList;
        public static XmlDocument xmlDroneBLList;

        /// <summary>
        /// Check if the UserAgent already exists
        /// </summary>
        /// <param name="Value"></param>
        /// <returns></returns>
        public static Boolean UserAgentExists(XmlDocument document, string value)
        {
            XmlNodeList entryNodes = document.SelectNodes("/gatekeeper/useragent/entry");
            foreach (XmlNode node in entryNodes)
            {
                string pattern = node.Attributes[@"useragent"].Value;
                GateKeeperModule.log.DebugFormat("Matching UserAgent entry : {0} = {1}", value, pattern);
                if (!string.IsNullOrEmpty(pattern) && Regex.IsMatch(value, pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase))
                {
                    GateKeeperModule.log.Debug("UserAgent match found");
                    return true; 
                }
            }
            GateKeeperModule.log.Debug("UserAgent match not found");
            return false;
        }

        ///// <summary>
        ///// Check if the IPAddress already exists
        ///// </summary>
        //public static Boolean IPEntryExists(XmlDocument document, string value)
        //{
        //    XmlNode node = document.SelectSingleNode(string.Format("/gatekeeper/ipaddress/entry[@ipaddress='{0}']", value));
        //    GateKeeperModule.log.DebugFormat((node != null) ? "IP Address found" : "IP Address not found");
        //    return (node != null);
        //}

        /// <summary>
        /// Search across all entries based on the ID value
        /// </summary>
        public static Boolean EntryExists(string id)
        {
            XmlNode node = xmlBlackList.SelectSingleNode(string.Format("/gatekeeper/*/entry[@id='{0}']", id));
            GateKeeperModule.log.DebugFormat((node != null) ? "ID found" : "ID not found");
            return (node != null);
        }

        public static void AddEntry(XmlFileType fileType, NodeType nodeType, string value, string comment)
        {
            XmlDocument document = getXmlDocByType(fileType);
            if (document == null) { return; }

            switch (nodeType)
            {
                case NodeType.ipaddress:
                    if (CheckIPAddress.IPEntryExists(document, value))
                    {
                        GateKeeperModule.log.DebugFormat("IPAddress already exists in {0} : [{1}]", fileType.ToString(), value);
                        return;
                    }
                    break;
                case NodeType.useragent:
                    if (UserAgentExists(document, value))
                    {
                        GateKeeperModule.log.DebugFormat("UserAgent already exists in {0} : [{1}]", fileType.ToString(), value);
                        return;
                    }
                    break;
            }

            XmlNode newNode = document.CreateElement("entry");
            newXmlAttribute(newNode, "id", System.Guid.NewGuid().ToString("N"));
            newXmlAttribute(newNode, "date", DateTime.Now.ToString());
            newXmlAttribute(newNode, nodeType.ToString(), value);
            newXmlAttribute(newNode, "comment", HttpUtility.HtmlEncode(comment));
            document.SelectSingleNode(string.Format("/gatekeeper/{0}", nodeType.ToString())).AppendChild(newNode);
            SaveXMLFile(document, fileType);
            GateKeeperModule.log.DebugFormat("New entry added to {0} : [{1}]", fileType.ToString(), value);

        }

        public static XmlDocument getXmlDocByType(XmlFileType fileType)
        {
            XmlDocument document = null;
            switch (fileType)
            {
                case XmlFileType.BlackList:
                    document = xmlBlackList;
                    break;
                case XmlFileType.WhiteList:
                    document = xmlWhiteList;
                    break;
                case XmlFileType.HoneyPotList:
                    document = xmlHoneyPotList;
                    break;
                case XmlFileType.HttpBLList:
                    document = xmlHttpBLList;
                    break;

                
                default:
                    break;
            }

            if (document == null)
            {
                GateKeeperModule.log.WarnFormat("Error resolving which Xml document to use");
                GateKeeperModule.log.ErrorFormat("XmlDocument Type Requested : [{0}]", fileType.ToString());
                return null;
            }

            return document;
        }

        /// <summary>
        /// Deletes an entry based on the id value and file type
        /// </summary>
        /// <param name="id">id value of an gatekeeper entry</param>
        public static void DeleteEntry(XmlFileType fileType, string id)
        {
            XmlDocument document = getXmlDocByType(fileType);
            if (document == null) { return; }
            
            XmlNode node = document.SelectSingleNode(string.Format("/gatekeeper/*/entry[@id='{0}']", id));
            if (node != null)
            {
                GateKeeperModule.log.DebugFormat("Removing entry from {0} : [{1}]", fileType.ToString(), id);
                node.ParentNode.RemoveChild(node);
                SaveXMLFile(document, fileType);
                GateKeeperModule.log.DebugFormat("Entry removed from {0} : [{1}]", fileType.ToString(), id);
                return;
            }
            GateKeeperModule.log.DebugFormat("Entry not found in {0} : [{1}]", fileType.ToString(), id);
        }

        /// <summary>
        /// Creates a new xml attribute for addition to the new XML Node
        /// </summary>
        /// <param name="newNode">Node declared in the calling function</param>
        /// <param name="name">Name of the attribute</param>
        /// <param name="value">Value of the attribute</param>
        public static void newXmlAttribute(XmlNode newNode, string name, string value)
        {
            XmlDocument Doc = newNode.OwnerDocument;
            XmlAttribute attr = Doc.CreateAttribute(name);
            attr.Value = value;
            newNode.Attributes.Append(attr);
        }

        public static XmlDocument GetXMLDocument(XmlFileType fileType)
        {
            GateKeeperModule.log.DebugFormat("Looking for xml data in cache : [{0}_cache]", fileType.ToString());

            string CacheKey = string.Format("{0}_cache", fileType.ToString());
            XmlDocument document = (XmlDocument)HttpRuntime.Cache[CacheKey];

            if (document != null)
            {
                GateKeeperModule.log.Debug("Xml data already in cache");
                return document;
            }

            string path = getXmlFilePath(fileType);
            GateKeeperModule.log.DebugFormat("Retrieving xml file : [{0}]", path);

            try
            {
                document = new XmlDocument();
                if (!File.Exists(path))
                {
                    GateKeeperModule.log.Debug("Creating new file");
                    NewXmlFile.Create(fileType, path);
                }

                GateKeeperModule.log.DebugFormat("Loading xml file : [{0}]", path);
                document.Load(path);

                if (document == null)
                {
                    throw new Exception(string.Format("Empty Xml document : [{0}]", path));
                }

                GateKeeperModule.log.Debug("Adding xml data to cache");
                HttpRuntime.Cache.Insert(CacheKey, document, new CacheDependency(path));
                return document;
            }
            catch (Exception ex)
            {
                GateKeeperModule.log.WarnFormat("GetXMLDocument : [{0}]", ex.Message);
                GateKeeperModule.log.Debug(ex.StackTrace);
                throw ex;
            }
        }

        private static string getXmlFilePath(XmlFileType fileType)
        {
            string fileName = string.Format("gk{0}.xml", fileType.ToString());
            string path = HttpContext.Current.Server.MapPath(string.Format("~/app_data/{0}", fileName));
            return path;
        }

        /// <summary>
        /// Locks a XML file and then saves the passed XmlDocument in the app_data folder
        /// </summary>
        public static void SaveXMLFile(XmlDocument _Document, XmlFileType fileType)
        {
            object _SyncRoot = new object();
            try
            {
                lock (_SyncRoot)
                {
                    string path = getXmlFilePath(fileType);
                    GateKeeperModule.log.DebugFormat("Saving xmlDocument : [{0}]", path);
                    _Document.Save(path); 
                }
            }
            catch (Exception ex)
            {
                GateKeeperModule.log.WarnFormat("SaveXMLFile : [{0}]", ex.Message);
                GateKeeperModule.log.Error(ex.StackTrace);
                throw ex;
            }
        }

        public static string ResolveUrl(string originalUrl)
        {
            if (!string.IsNullOrEmpty(originalUrl) && '~' == originalUrl[0])
            {
                int index = originalUrl.IndexOf('?');
                string queryString = (-1 == index) ? null : originalUrl.Substring(index);
                if (-1 != index) originalUrl = originalUrl.Substring(0, index);
                originalUrl = VirtualPathUtility.ToAbsolute(originalUrl) + queryString;
            }

            return originalUrl;
        }

        public static void SaveStream(MemoryStream ms, string path)
        {
            object _SyncRoot = new object();
            try
            {
                lock (_SyncRoot)
                {
                    GateKeeperModule.log.DebugFormat("Saving memory stream to {0}", path);
                    using (FileStream fs = File.Open(path, FileMode.Create, FileAccess.Write))
                    {
                        ms.WriteTo(fs);
                        ms.Dispose();
                    }
                    GateKeeperModule.log.Debug("Xml file saved");
                }
            }
            catch (Exception ex)
            {
                GateKeeperModule.log.WarnFormat("SaveStream error : [{0}]", ex.Message);
                GateKeeperModule.log.Error(ex.StackTrace);
                throw ex;
            }
            finally
            {
                if (ms != null)
                {
                    GateKeeperModule.log.Debug("Clearing memory stream");
                    ms.Dispose();
                }
            }
        }

        public static void Begin_NewRequest(HttpContext current)
        {
            GateKeeperModule.log.Debug("*** Begin new request ***");
            GateKeeperModule.log.DebugFormat("Absolute Url : [{0}]", current.Request.Url.AbsoluteUri);
            GateKeeperModule.log.DebugFormat("User Agent : [{0}]", current.Request.UserAgent ?? "empty");
            GateKeeperModule.log.DebugFormat("User Referrer : [{0}]", (current.Request.UrlReferrer == null) ? "empty" : current.Request.UrlReferrer.ToString());
            GateKeeperModule.log.DebugFormat("User Host Address : [{0}]", current.Request.UserHostAddress);
        }

        public static void End_NewRequest(HttpContext current)
        { GateKeeperModule.log.Debug("*** End of request ***"); }

        /// <summary>
        /// Performs a DNS Lookup using an async call and terminates
        /// after specified period of time has passed
        /// </summary>
        /// <param name="hostName">www.dscoduc.com</param>
        /// <returns>192.168.1.100</returns>
        private delegate IPHostEntry DnsLookupHandler(string hostName);
        public static string DnsLookup(string hostName, int timeout)
        {
            string response = null;
            try
            {
                DnsLookupHandler callback = new DnsLookupHandler(Dns.GetHostEntry);
                IAsyncResult result = callback.BeginInvoke(hostName, null, null);
                if (result.AsyncWaitHandle.WaitOne(timeout, false))
                {   // Received response within timeout limit
                    IPHostEntry he = callback.EndInvoke(result);
                    IPAddress[] ip_addrs = he.AddressList;
                    if (ip_addrs.Length > 0)
                        response = ip_addrs[0].ToString();
                }
                return response;
            }
            catch (System.Exception ex)
            {
                GateKeeperModule.log.WarnFormat("DnsLookup : [{0}]", ex.Message);
                GateKeeperModule.log.Error(ex.StackTrace);
                return response;
            }
        }
        /// <summary>
        /// Converts a forward IP address into a reversed IP Address
        /// suitable for dnsBl to process in the query
        /// </summary>
        /// <param name="ipaddress">ex. 192.168.1.100</param>
        /// <returns>ex. 100.1.168.192</returns>
        public static string flipIPAddress(string ipaddress)
        {
            string returnString = null;
            try
            {
                // Split string into four octects
                string[] octects = ipaddress.Split(new char[] { '.' }, 4);
                // Build new string in reverse
                for (int i = octects.Length - 1; i >= 0; i--)
                    returnString += string.Format("{0}.", octects[i]);

                // Remove trailing period
                returnString = returnString.Trim(new char[] { '.' });
                return returnString;
            }
            catch (Exception ex)
            {
                GateKeeperModule.log.WarnFormat("flipIPAddress : [{0}]", ex.Message);
                GateKeeperModule.log.Error(ex.StackTrace);
                return returnString;
            }
        }

    }

    #region public enums
    public enum NodeType
    { ipaddress, useragent, url }

    public enum AttributeName
    { ID,Date,IPAddress,UserAgent,Comment }

    public enum Violation
    { IPAddress,EmptyUserAgent,UserAgent,Hotlink,HoneyPot,HttpBL,ProxyBL,DroneBL }

    public enum XmlFileType
    { BlackList, WhiteList, HoneyPotList, HttpBLList, ProxyBLList, DroneBLList }

    #endregion
}
